概述
常说Objective-C是一门动态语言,那么问题来了,这个动态
表现在那些方面呢?
其实最主要的表现就是Objective-C将很多静态语言在编译和链接时做的事情放到了运行时去做,它在运行时实现了对类、方法、成员变量、属性等信息的管理机制。
同时,运行时机制为我们开发过程提供很多便利之处,比如:
- 在运行时创建或者修改一个类;
- 在运行时修改成员变量、属性等;
- 在运行时进行消息分发和分发绑定;
……
与之对应实现的就是Objective-C的Runtime机制。
Objective-C的Runtime目前有两个版本:Leagcy Runtime
和Moden Runtime
。Leagcy Runtime
是最早期给32位Mac OX Apps
使用的,而Moden Runtime
是给64位Mac OX Apps
和iOS Apps
使用的。
Runtime基本是C和汇编编写的,有一系列函数和数据结构组成的,具有公共接口的动态共享库,可见苹果为了动态系统的高效而作出的努力,你可以在这里下载到苹果维护的开源代码。
同时,GNU也有一个开源的Runtime版本,他们在努力保持一致。其头文件都存放在/usr/include/objc
目录下。在Objective-C Runtime Reference中,有对Runtime函数使用细节的文档。
类与对象
类的数据结构(Class)
类的数据结构可以在objc/runtime.h
源码中找到,如下所示:
|
|
在Objective-C中类
是由Class
表示的,Class
是一个指向struct objc_class
的指针。
在这个类的数据结构中,有几个字段需要解释一下:
isa:在大多数的面向对象的语言中,都有类和对象的概念。其中,对象是类的实例,是通过类数据结构的定义创建出来的,对象的isa指针是指向其所属类的。同时,在Objective-C语言中,类本身也是一个对象,类作为对象时isa指针指向元类(Meta Class),后面会详解;
super_class:指向该类的父类,如果该类已经是根类(NSObject 或 NSProxy),则 其super_class 为NULL;
version:该字段可以获取类的版本信息,在对象的序列化中可以通过类的版本信息来标识出不同版本的类定义中实例变量布局的改变。
objc_cache与cache
上文object_class
中结构体中的cache
字段,是用来缓存使用过的方法。这个字段是一个指向objc_cache
的指针,具体数据结构如下所示:
字段的具体描述如下:
- mask:整数类型,指定分配的缓存
bucket
的总数。在方法查找过程中,Objective-C runtime
使用这个字段来确定开始线性查找数组的索引位置。指向方法selector
的指针与该字段做一个AND
位操作(index = (mask & selector))。这可以看作为一个简单的hash
散列算法; occupied
:一个整数,指定实际占用的缓存bucket
的总数;buckets
:指向Method
数据结构指针的数组。这个数组可能包含不超过mask+1
个元素。需要注意的是,指针可能是NULL
,表示这个缓存bucket
没有被占用。另外被占用的bucket
可能是不连续的。这个数组可能会随着时间而增长。
关于上文object_class
中结构体中的cache
字段,对它的解释如下:
- cache:用于缓存最近使用的方法,一个对象可响应的方法列表中通常只有一部分是经常被调用的,cache 则是用来缓存最常调用的方法,从而避免每次方法调用时都去查找对象的整个方法列表,提升性能。
- 在一些结构较为复杂的类关系中,一个对象的响应方法可能来自于继承的类结构中,此情况下查找相应的响应方法时就会比较耗时,通常使用cache缓存可以减低查找时间;
举个栗子:
其缓存调用方法的流程:
1、
[NSDictionary alloc]
先被执行。由于NSDictionary
没有+alloc
方法,于是去父类NSObject
中去查找;2、 检测
NSObject
是否响应+alloc
方法,发现响应,于是检测NSDictionary
类,并根据其所需的内存空间大小开始分配内存空间,然后把isa
指针指向NSDictionary
类。同时,+alloc
方法也被加进对应类的cache
列表里;- 3、执行
-init
方法,如果NSDictionary
响应该方法,则直接将该方法加入cache
列表;如果不响应,则去父类查找; - 4、 在后期的操作中,如果再以
[[NSDictionary alloc] init]
这种方式来创建数组,则会直接从cache
中取出相应的方法,直接调用。
元类(Meta Class)
上面讲到,有时候类也是一个对象,这种类对象是某一种类的实例,这种类就是元类(Meta Class)。
好比类与对应的实例描述一样,元类则是类作为对象的描述。元类中方法列表对应的是类方法(Class Method)列表,这正是类作为一个对象所需要的。
当调用该方法[NSArray alloc]
时,Runtime就会在对应的元类方法列表查找其类对应的方法,并匹配调用。
|
|
官方的解释如下所示:
Since a class is an object, it must be an instance of some other class: a metaclass. The metaclass is the description of the class object, just like the class is the description of ordinary instances. Class methods are described by the metaclass on behalf of the class object, just like instance methods are described by the class on behalf of the instance objects.
至此,又有了新的疑问:元类又是谁的实例呢?它的isa又指向谁呢?答案如下图所示:
由上图可以看出,元类的isa都指向根元类(Root Meta Class),即元类都是根元类的实例。
而根元类(Root Meta Class)的isa则指向自己,这样就不会无休止的关联下去了。
图中同样展示类和元类的继承关系,非常清晰易懂。
类的实例数据结构
在 Objective-C 中类的实例的数据结构是定义在struct objc_object
中(objc/objc.h):
可以看出,这个结构体只有一个字段,即指向该实例所属类的isa
指针。
这个指针跟上面介绍的类的isa
不一样:类的isa
指向对应的元类(Meta Class
),实例的isa
则是指向对应的类(Class
),而这个Class
里包含上述所讲的数据:父类、类名、方法列表等等。
当我们向一个类的实例发送消息时,Runtime
会根据实例对象的isa
找到这个实例对象所属的类,然后再在这个类的方法列表和其父类的方法列表中查找与消息相对应的selector
指向的方法,进而执行目标方法。
当创建某一个类的实例时,分配的内存中会包含一个objc_object
数据结构,然后是类的实例变量的相关数据。
NSObject
类的alloc
和allocWithZone:
方法是使用函数class_createInstance
来创建objc_object
数据结构。
我们常见的id
是一个struct objc_object
类型的指针。id
类型的对象可以转换为任何一种类型的对象,它的作用有点类似 C 语言中的 void *
指针类型。
相关函数
Objective-C的Runtime
我们提供了很多运行时状态跟类与对象相关的函数。类的操作方法大部分是以class_
为前缀的,而对象的操作方法大部分是以objc_
或object_
为前缀,具体以分类的形式进行讨论。
类相关函数
类的相关函数大部分是与objc_class
结构体各个字段相关的方法。
类名
|
|
- 如果
cls
传入nil
时,则返回nil
字符串;
父类(super_class)和元类(meta_class)
|
|
class_getSuperclass
函数,当cls
为Nil
或者cls
为根类时,返回Nil
。我们可以使用NSObject
类的superclass
方法来达到同样的目的;class_isMetaClass
函数,如果是cls
是元类,则返回YES;如果否或者传入的cls
为Nil
,则返回NO
。
实例变量大小(instance_size)
|
|
成员变量(ivars)及属性
在objc_class
中,所有的成员变量、属性的信息是放在链表ivars
中的。ivars
是一个数组,数组中每个元素是指向Ivar
(变量信息)的指针。
1、成员变量操作函数:
class_getInstanceVariable
函数,它返回一个指向包含name
指定的成员变量信息的objc_ivar
结构体的指针(Ivar
);class_getClassVariable
函数,目前没有找到关于Objective-C
中类变量的信息,一般认为Objective-C
不支持类变量。注意,返回的列表不包含父类的成员变量和属性;Objective-C
不支持往已存在的类中添加实例变量,因此不管是系统库提供的类,还是我们自定义的类,都无法动态添加成员变量;当通过运行时来创建一个类的时候,我们就可以使用
class_addIvar
函数。不过需要注意的是,这个方法只能在objc_allocateClassPair
函数与objc_registerClassPair
之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是
1<<alignment
。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))
;class_copyIvarList
函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar
结构体的指针。这个数组不包含在父类中声明的变量。outCount
指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。
2、属性相关的操作函数:
3.MAC OS X
系统支持使用垃圾回收器,Runtime
提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理strong/weak
引用。这几个函数定义如下:
通常情况下,我们不需要去主动调用这些方法;在调用objc_registerClassPair
时,会生成合理的布局。
方法(methodLists)
|
|
class_addMethod
的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用
method_setImplementation
。一个Objective-C
方法是一个简单的C
函数,它至少包含两个参数self
和_cmd
。所以,我们的实现函数(IMP
参数指向的函数)至少需要两个参数,如下所示:
|
|
与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否已存在。
参数types
是一个描述传递给方法的参数类型的字符数组,这就涉及到类型编码。
class_getInstanceMethod
、class_getClassMethod
函数,与class_copyMethodList
不同的是,这两个函数都会去搜索父类的实现;class_copyMethodLis
t函数,返回包含所有实例方法的数组。如果需要获取类方法,则可以使用class_copyMethodList(object_getClass(cls), &count)
(一个类的实例方法是定义在元类里面)。该列表不包含父类实现的方法。outCount
参数返回方法的个数,在获取到方法列表后,我们需要使用free()
方法来释放它;class_replaceMethod
函数,该函数的行为可以分为两种:如果类中不存在name
指定的方法,则类似于class_addMethod
函数一样会添加方法;如果类中已存在name
指定的方法,则类似于method_setImplementation
一样去替代原方法的实现;class_getMethodImplementation
函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比
method_getImplementation(class_getInstanceMethod(cls, name))
更快。返回的函数指针可能是一个指向runtime
内部的函数,而不一定是方法的实现。例如,如果类实例无法响应
selector
,则返回的函数指针将是运行时消息转发机制的一部分;class_respondsToSelector
函数,我们通常使用NSObject
类的respondsToSelector:
或者instancesRespondToSelector:
方法来达到相同目的。
协议(objc_protocol_list)
|
|
class_conformsToProtocol
函数可以使用NSObject
类的conformsToProtocol:
方法来替代;class_copyProtocolList
函数返回的是一个数组,在使用后我们需要使用free()
手动释放。
版本(version)
|
|
其它
runtime
还提供了两个不直接使用的函数来供CoreFoundation
的tool-free bridging
使用,即:
使用上述函数时,需要特别的注意一下细节信息和使用规范,具体可以查阅 Objective-C Runtime Reference。
动态创建类与对象
Runtime提供在运行时创建类与对象的方法。
动态创建类
|
|
objc_allocateClassPair
函数:如果我们要创建一个根类,则superclass
指定为Nil
。extraBytes
通常指定为0,该参数是分配给类和元类对象尾部的索引ivars
的字节数;创建一个新类,首先,我们需要调用
objc_allocateClassPair
。然后使用诸如class_addMethod
、class_addIvar
等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用
objc_registerClassPai
r函数来注册类,之后这个新类就可以在程序中使用了;- 实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上;
objc_disposeClassPair
函数用于销毁一个类,不过需要注意的是,如果程序运行中还存在类或其子类的实例,则不能调用针对类调用该方法,在后面的栗子中也有该方面的讲解。
动态创建对象
|
|
class_createInstance
函数:创建实例时,会在默认的内存区域为类分配内存。extraBytes
参数表示分配的额外字节数。这些额外的字节可用于存储在类定义中所定义的实例变量之外的实例变量,该函数在ARC环境下无法使用 ;- 调用
class_createInstance
的效果与+alloc
方法类似。不过在使用class_createInstance
时,我们需要确切的知道我们要用它来做什么。在下面的例子中,我们用NSString来测试一下该函数的实际效果:12345678- (void)testInstanceMethod{id theObject = class_createInstance([NSString class], sizeof(unsigned));id str1 = [theObject init];NSLog(@"%@", [str1 class]);id str2 = [[NSString alloc] initWithString:@"test"];NSLog(@"%@", [str2 class]);}
输出结果:
- 可以看到,使用
class_createInstance
函数获取的是NSString
实例,而不是类簇中的默认占位符类__NSCFConstantString
; objc_constructInstance
函数:在指定的位置(bytes
)创建类实例;objc_destructInstance
函数:销毁一个类的实例,但不会释放并移除任何与其相关的引用;
实例操作函数
实例操作函数主要是针对我们创建的实例对象的一系列操作函数。
我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。这组函数可以分为三小类:
1、针对整个对象进行操作的函数,这类函数包含:
有这样一种场景,假设我们有类A和类B,且类B是类A的子类。
类B通过添加一些额外的属性来扩展类A。现在我们创建了一个A类的实例对象,并希望在运行时将这个对象转换为B类的实例对象,这样可以添加数据到B类的属性中。
这种情况下,我们没有办法直接转换,因为B类的实例会比A类的实例更大,没有足够的空间来放置对象。此时,我们就要以使用以上几个函数来处理这种情况,如下代码所示:
2、针对对象实例变量进行操作的函数,这类函数包含:
如果实例变量的Ivar
已经知道,那么调用object_getIvar
会比object_getInstanceVariable
函数快,相同情况下,object_setIvar
也比object_setInstanceVariable
快。
3、针对对象的类进行操作的函数,这类函数包含:
获取类定义
Objective-C
动态运行库会自动注册我们代码中定义的所有的类。
我们也可以在运行时创建类定义并使用objc_addClass
函数来注册它们。Runtime
提供了一系列函数来获取类定义相关的信息,这些函数主要包括:
objc_getClassList
函数:获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject
体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。
下面代码演示了该函数的用法:
|
|
输出的结果:
获取类定义的方法有三个:
objc_lookUpClass
、objc_getClass
和objc_getRequiredClass
。如果类在运行时未注册,则
objc_lookUpClass
会返回nil
,而objc_getClass
会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil
。- 而
objc_getRequiredClass
函数的操作与objc_getClass
相同,只不过如果没有找到类,则会杀死进程。 objc_getMetaClass
函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。
运行时操作操作类与对象的示例代码
- #####实例、类、父类、元类关系结构的示例代码
首先,创建继承关系为Animal->Dog->NSObject
的几个类,然后使用Runtime的方法打印其中的关系,运行结果如下所示:
|
|
打印信息如下所示:
需要特别注意一下,Object_getClass
可以获取当前对象的isa
。以Dog
类打印信息为例,解释一下具体实现的原理:
- 首先,通过
Object_getClass
获取实例aDog
的Class(isa)为Dog
; - 然后,通过
class_getSuperclass
获取Dog
的父类为Animal
类; - 通过
objc_getMetaClass
指定类名,获取对应的元类,通过class_isMetaClass
方法可以判断一个类是否为指定类的元类,这里确认后,打印出YES
,打印出的元类名称为Dog
;打印元类父类为Animal
;在通过Object_getClass
获取元类的isa,指向NSObject
。
同理可得,Animal
和UIView
打印信息解释同上。NSobject
,它元类的isa指针还是指向自己的类——NSobject
。打印的信息与上述的关系图保持一致。
动态操作类与实例的代码
动态创建类的源码
|
|
打印的信息如下所示:
在执行objc_allocateClassPair
中,类的名称设置为Cat
时,创建出的Class
的地址始终指向0x0
,创建类失败。
猜测其中的原因可能是Cat
与内部的关键字冲突了,导致类创建失败,改为cat
或者其他的都可以创建成功;
- 在上面的代码中,在运行时动态创建了
Animal
的一个子类:Lion
;接着为这个类添加了方法和实现; - 打印了
Lion
的类、父类、元类相关信息; - 遍历和打印了
Lion
的方法的相关信息; - 调用了
Lion
的方法; - 最后销毁了实例和类。
针对上述代码,有几点需要特殊说明一下:
- 对于
#pragma clang diagnostic...
几行代码,是用于忽略编译器对于未声明@selector的警告信息的,在代码中,我们动态地为一个类添加方法,不会事先声明的; class_addMethod()
函数的最后一个参数types
是描述方法返回值和参数列表的字符串。我们的代码中的用到的
i@:@
四个字符分别对应着:返回值int32_t
、参数id self
、参数SEL _cmd
、参数NSDictionary *dic
。这个其实就是类型编码(Type Encoding)的概念。在 Objective-C 中,为了协助 Runtime 系统,编译器会将每个方法的返回值和参数列表编码为一个字符串,这个字符串会与方法对应的 selector 关联。更详细的知识可以查阅 Type Encodings;- 使用
objc_registerClassPair()
函数需要注意,不能注册已经注册过的类; - 使用
objc_disposeClassPair()
函数时需要注意,当一个类的实例和子类还存在时,不能去销毁一个类,谨记;
isKindOf 和 isMemberOf
举个栗子:
关于isMemberOfClass和isKindOfClass在Object.mm中的实现,具体如下:
在result1中,从isMemberOf
看出NSObject class
的isa
第一次会指向NSObject
的Meta Class
,因此NSObject
的Meta Class
与NSObject class
是不相等,返回FALSE
;
在result2中,isKindOf
第一次指向NSObject
的Meta Class
,接着执行superclass
时,根元类NSObject
的Meta Class
根据上面所讲的其superclass
指针会闭环指向NSObject class
,从而结果值为TRUE
;
在result3中,isa
会指向TestMetaClass
的Meta Class
,与TestMetaClass Class
不相等,结果值为FALSE
;
在result4中,第一次是TestMetaClass Meta Class
,第二次super class
后就是NSObject Meta Class
,结果值为FALSE
;
以上再次验证了,NSObject Meta Class
的isa
指针指向自身,其super class
指向NSObject
。
小结
本文着重讲解了在Runtime时类与对象相关方法和数据结构,通过这些讲解可以让大家对Objective-C底层类与对象实现有大致的了解,并且可以为大家平常编程过程提供一些思路上的启发。
测试使用的栗子(Demo)都在篮子里